Creating a Virtual Piano

Playing with additive synthesis in Python.
First, some necessary imports:
import matplotlib.pyplot as plt
import numpy as np
import IPython as ju
from IPython.display import Audio
plt.rcParams["figure.figsize"] = (20,5)
The following equation will give the frequency \(f\) of the \(n^{th}\) key: \[f(n) = 440 * 2^\frac{n-49}{12}\]
This equation is from here .
We use this as a utility function named key2freq in the VirtualPiano class bellow.
class VirtualPiano:
def __init__(self,
t_duration,
sample_freq=48000):
self.t_duration = t_duration
self.sample_freq = sample_freq
self.sequence = []
# elements to pass through sine function:
self.t = np.linspace(0, t_duration,
num=int(self.sample_freq*self.t_duration))
def sound(self, freq: float):
t = self.t
Y = np.sin(2 * np.pi * freq * t) * np.exp(-0.0004 * 2 * np.pi * freq * t)
Y += np.sin(2 * 2 * np.pi * freq * t) * np.exp(-0.0004 * 2 * np.pi * freq * t) / 2
Y += np.sin(3 * 2 * np.pi * freq * t) * np.exp(-0.0004 * 2 * np.pi * freq * t) / 4
Y += np.sin(4 * 2 * np.pi * freq * t) * np.exp(-0.0004 * 2 * np.pi * freq * t) / 8
Y += np.sin(5 * 2 * np.pi * freq * t) * np.exp(-0.0004 * 2 * np.pi * freq * t) / 16
Y += np.sin(6 * 2 * np.pi * freq * t) * np.exp(-0.0004 * 2 * np.pi * freq * t) / 32
Y += Y * Y * Y
Y *= 1 + 16 * t * np.exp(-6 * t)
return Y
def key2freq(self, n: int):
"""Input: 0-88 (piano key) (0 for pause)
Output: frequency of key in hertz"""
if 0 < n and n <= 88:
return 440 * 2**((n-49)/12)
elif n == 0:
return 0
else:
raise ValueError("Only keys 0-88 allowed")
def waveform(self):
"""Generate continuous waveform
of the sequence of notes/frequencies."""
waveform = []
for key in self.sequence:
freq = self.key2freq(key)
oscillator = self.sound(freq)
for val in oscillator:
waveform.append(val)
return np.asarray(waveform)
Example: Scale of C-Major
# Initialize piano with duration of 0.5s for each note:
piano = VirtualPiano(t_duration=0.5)
# Add sequence of keys to play:
piano.sequence = [40, 42, 44, 45, 47, 49, 51]
# Play waveform of notes with sampling frequency of 48 kHz
c_major_scale_waveform = piano.waveform()
Audio(c_major_scale_waveform, rate=48000)
Example: Prelude in C Major - Bach
# Initialize new piano
piano = VirtualPiano(t_duration=0.175)
# Sequence of keys:
piano.sequence = [40, 44, 47, 52, 56, 47, 52, 56,
40, 44, 47, 52, 56, 47, 52, 56,
40, 42, 49, 54, 57, 49, 54, 57,
40, 42, 49, 54, 57, 49, 54, 57,
39, 42, 47, 54, 57, 47, 54, 57,
40, 44, 47, 52, 56, 47, 52, 56,
40, 44, 47, 52, 56, 47, 52, 56,
40, 44, 49, 56, 61, 49, 56, 61,
40, 44, 49, 56, 61, 49, 56, 61,
40, 42, 46, 49, 54, 46, 49, 54,
40, 42, 46, 49, 54, 46, 49, 54,
39, 42, 47, 54, 59, 47, 54, 59,
39, 42, 47, 54, 59, 47, 54, 59,
39, 40, 44, 47, 52, 44, 47, 52,
39, 40, 44, 47, 52, 44, 47, 52,
37, 40, 44, 47, 52, 44, 47, 52,
37, 40, 44, 47, 52, 44, 47, 52,]
prelude_waveform = piano.waveform()
plt.plot(prelude_waveform[:20000])
Audio(prelude_waveform, rate=48000)

framerate = 44100 # 44.1 kHz sampling frequency
t = np.linspace(0, 5, num=framerate*5)
sound = np.sin(np.pi*220*t) + np.sin(np.pi*330*t)
print("Framerate=44100")
plt.plot(np.asarray([*range(1000)]), sound[:1000], "go")
plt.ylabel("sampling_freq:\n44.1kHz")
plt.show()
Framerate=44100

Audio(data, rate=framerate)
t_dur = 0.5
samp = 44100
x = np.linspace(0.0, t_dur, num=int(t_dur*samp))
np.mod(x ,1)
array([0.00000000e+00, 2.26767654e-05, 4.53535308e-05, ...,
4.99954646e-01, 4.99977323e-01, 5.00000000e-01])
t = np.linspace(0,5, num=44100*5)
freq = 32.7032 # this is the frequency of the 4th key, C,contra_octave
data = 0.3 * np.sin(1*np.pi*np.sin(np.pi*t**2)*32.7032)
data += np.sin(np.pi*np.sin(np.pi*t**2)*65.4064) / 4
data += np.sin(np.pi*np.sin(np.pi*t**2)*130.813) / 8
reverb = lambda arr: np.concatenate((np.zeros(88200), arr))
data += reverb(data)[:220500]
plt.plot(np.concatenate((data[:50000]))
Audio((data), rate=44100)

t = np.linspace(0,0.25, num=int(48000*0.25))
freq = 220
s = np.sin(2 * np.pi * freq * t) * np.exp(-0.0004 * 2 * np.pi * freq * t) + np.sqrt(np.pi * 220 * t)/1200
s = np.concatenate((s,s,s,s,s,s,s,s))
plt.plot(s)
Audio(s, rate=48000)

fade_in = np.sin(np.pi * 220 * t) * np.sqrt(np.pi * 220 * t)
fade_ins = np.concatenate((fade_in, fade_in, fade_in, fade_in))
fade_out = np.sin(np.pi * 220 * t) * np.exp(-0.000004 * t * 220)
fade_outs = np.concatenate((fade_out, fade_out, fade_out, fade_out))
print("Fade-in:")
ju.display.display(Audio(fade_ins, rate=48000))
plt.plot(fade_ins)
plt.show()
print("Fade-out:")
ju.display.display(Audio(fade_outs, rate=48000))
plt.plot(fade_outs)
plt.show()
Fade-in:

Fade-out:

Non-trigonometric Oscillators
Sawtooth Oscillators
\(f(t) = mod(t, 1)\)
This is a WIP.
t = np.linspace(0, 5, num=48000*5)
sawtooth = lambda t_i: -(2/np.pi) * np.asarray([*sum((((-1)**k)/k)*np.sin(2*np.pi*k*t_i*440) for k in range(1, 20000))])
# wav = (sawtooth(t))
# ju.display.display(Audio(wav, rate=48000))
# plt.plot(wav[:20000])
# plt.show()